package com.gammadevs.gwt.d3charts.client.widgets;

import com.github.gwtd3.api.D3;
import com.github.gwtd3.api.arrays.Array;
import com.github.gwtd3.api.core.Selection;
import com.github.gwtd3.api.core.Transition;
import com.github.gwtd3.api.core.Value;
import com.github.gwtd3.api.functions.DatumFunction;
import com.github.gwtd3.api.scales.LinearScale;
import com.github.gwtd3.api.svg.Area;
import com.github.gwtd3.api.svg.Axis;
import com.github.gwtd3.api.svg.Brush;
import com.github.gwtd3.api.svg.Line;
import com.github.gwtd3.api.time.TimeScale;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsDate;
import com.google.gwt.dom.client.BrowserEvents;
import com.google.gwt.dom.client.Element;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTMLPanel;

import java.util.*;

public class ClientTradeMap extends Composite {

    interface ClientTradeMapUiBinder extends UiBinder<HTMLPanel, ClientTradeMap> {}
    private static ClientTradeMapUiBinder ourUiBinder = GWT.create(ClientTradeMapUiBinder.class);

    private final double focusRatio = 0.2;
    private final int chartBottomMargin = 15;

    public interface Bundle extends ClientBundle {
        public static final Bundle INSTANCE = GWT.create(Bundle.class);

        @Source("AxisComponentStyles.css")
        public MyResources css();
    }

    interface MyResources extends CssResource {
        String area();
        String axis();
        String x();
        String y();
        String line();
        String svg();
        String minor();
        String extent();
        String brush();
    }

    private final String id = "id";

    @UiField Element svgElement;
    @UiField HTMLPanel panel;

    public ClientTradeMap() {
        Bundle.INSTANCE.css().ensureInjected();
        initWidget(ourUiBinder.createAndBindUi(this));
        svgElement.setId(id);
        new Timer() {
            @Override
            public void run() {
                initChart(getMidMarketData(), getSpotRiskData());
            }
        }.schedule(5000);
    }

    private void initChart(final List<MidMarketData> values, final List<SpotRiskData> spotRiskData) {
        final MyResources css = Bundle.INSTANCE.css();
        final Margins margins = new Margins(40, 40, 40, 40);
        int height = panel.getElement().getClientHeight();
        int width = panel.getElement().getClientWidth();
        if (height <= 80 || width <= 80) {
            return;
        }
        svgElement.setAttribute("height", height + "px");
        svgElement.setAttribute("width", width + "px");
        final int w = panel.getElement().getClientWidth() - margins.left - margins.right;
        final int totalHeight = panel.getElement().getClientHeight() - margins.top - margins.bottom - chartBottomMargin;
        final int chartH = (int) (totalHeight * (1 - focusRatio));
        final int focusH = (int) (totalHeight * focusRatio);

        // Scales and axes. Note the inverted domain for the y-scale: bigger is
        // up!
        final TimeScale chartX  = D3.time().scale().range(0, w);
        final TimeScale focusX = D3.time().scale().range(0, w);
        final LinearScale chartY = D3.scale.linear().range(chartH, 0);
        final LinearScale focusY = D3.scale.linear().range(focusH, 0);

        final Axis xAxis = D3.svg().axis().scale(chartX).tickSize(-chartH);
        final Axis focusXAxis = D3.svg().axis().scale(chartX).tickSize(-focusH);
        final Axis yAxis = D3.svg().axis().scale(chartY).orient(Axis.Orientation.LEFT).ticks(5);

        final Brush brush = D3.svg().brush().x(focusX);
        final Area chartArea = D3.svg().area().interpolate(Area.InterpolationMode.MONOTONE)
                .x(new DatumX(chartX)).y0(chartH).y1(new DatumY(chartY));
        final Line chartLine = D3.svg().line().interpolate(Line.InterpolationMode.MONOTONE)
                .x(new DatumX(chartX)).y(new DatumY(chartY));
        final Line focusLine = D3.svg().line().interpolate(Line.InterpolationMode.MONOTONE)
                .x(new DatumX(focusX)).y(new DatumY(focusY));

        chartX.domain(Array.fromObjects(values.get(0).getDate(), values.get(values.size() - 1).getDate()));
        focusX.domain(chartX.domain());
        chartY.domain(Array.fromDoubles(0.933, getMaxY(values)));
        focusY.domain(chartY.domain());

        final Selection svg = D3
                .select("#id")
                //.append("rect")
                .attr("width", w + margins.left + margins.right)
                .attr("height", totalHeight + margins.top + margins.bottom);

        Selection chart = svg.append("g").attr("class", "chart")
                .attr("transform", "translate(" + margins.left + "," + margins.top + ")");

        Selection focus = svg.append("g").attr("class", "focus")
                .attr("transform", "translate(" + margins.left + "," + (margins.top + chartH + chartBottomMargin) + ")");

        // Add the x-axis.
        final Selection chartXAxis = chart.append("g").attr("class", css.x() + " " + css.axis())
                .attr("transform", "translate(0," + chartH + ")")
                .call(xAxis);

        // Add the y-axis.
        chart.append("g").attr("class", css.y() + " " + css.axis())
                .attr("transform", "translate(0,0)")
                .call(yAxis);

        final Selection areaPath = chart.append("path").attr("class", css.area())
                .attr("d", chartArea.generate(values));
        final Selection linePath = chart.append("path").attr("class", css.line())
                .attr("d", chartLine.generate(values));

        // Add the focus line path.

        focus.append("g").attr("class", css.x() + " " + css.axis())
                .attr("transform", "translate(0," + focusH + ")")
                .call(focusXAxis);

        focus.append("path").attr("class", css.line())
                .attr("transform", "translate(0,0)")
                .attr("d", focusLine.generate(values));

        focus.append("g")
                .attr("class", css.x() + " " + css.brush())
                .call(brush)
                .selectAll("rect")
                .attr("y", -6)
                .attr("height", focusH + 7);

        brush.on(Brush.BrushEvent.BRUSH, new DatumFunction<Void>() {
            @Override
            public Void apply(Element element, Value value, int i) {
                chartX.domain(brush.empty() ? focusX.domain() : brush.extent());
                areaPath.attr("d", chartArea.generate(values));
                linePath.attr("d", chartLine.generate(values));
                chartXAxis.call(xAxis);
                return null;
            }
        });

        /*
        //circles
        svg.selectAll(".risk").data(spotRiskData).enter().append("circle").attr("r", 8)
                .attr("cx", new DatumFunction<Double>() {
                    @Override
                    public Double apply(Element element, Value value, int i) {
                        return chartX.apply(value.<SpotRiskData>as().getDate()).asDouble();
                    }
                }).attr("cy", new DatumFunction<Double>() {
            @Override
            public Double apply(Element element, Value value, int i) {
                return chartY.apply(value.<SpotRiskData>as().getMidMarketPrice()).asDouble();
            }
        }).attr("fill", new DatumFunction<String>() {
            @Override
            public String apply(Element element, Value value, int i) {
                return value.<SpotRiskData>as().getPnl() >= 0 ? "rgba(0,150,0,0.5)" : "rgba(150,0,0,0.5)";
            }
        });
*/

        // On click, update the x-axis.
        /*svg.on(BrowserEvents.CLICK, new DatumFunction<Void>() {
            @Override
            public Void apply(final Element context, final Value d, final int index) {
                int n = values.size() - 1;
                int i = (int) Math.floor((Math.random() * n) / 2);
                int j = i + (int) Math.floor((Math.random() * n) / 2) + 1;
                chartX.domain(Array.fromObjects(values.get(i).getDate(), values.get(j).getDate()));
                Transition transition = svg.transition().duration(750);
                transition.select("." + css.x() + "." + css.axis()).call(xAxis);
                transition.select("." + css.area()).attr("d", chartArea.generate(values));
                transition.select("." + css.line()).attr("d", chartLine.generate(values));
                return null;
            };
        });*/


    }

    private double getMaxY(List<MidMarketData> values) {
        return Collections.max(values, new Comparator<MidMarketData>() {
            @Override
            public int compare(MidMarketData o1, MidMarketData o2) {
                return new Double(o1.getPrice()).compareTo(o2.getPrice());
            }
        }).getPrice();
    }

    private List<MidMarketData> getMidMarketData() {
        List<MidMarketData> midMarketData = new LinkedList<>();
        JsDate date = JsDate.create(2015, 0, 1, 22, 0, 0);
        midMarketData.add(new MidMarketData(date, 0.93334)); //22
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93347));
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93318));
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93337));
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93368)); //23
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93354));
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93376));
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93386));
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93401)); //00
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93392));
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.9343));
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93428));
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93426)); //01
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93408));
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93422));
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93434));
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93433)); //02
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93426));
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93453));
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93438));
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93423));//03
        date = increment(date, 15);
        midMarketData.add(new MidMarketData(date, 0.93428));
        return midMarketData;
    }

    private JsDate increment(JsDate date, int minutes) {
        return JsDate.create(date.getTime() + minutes * 60 * 1000);
    }

    private List<SpotRiskData> getSpotRiskData() {
        List<SpotRiskData> data = new LinkedList<>();
        data.add(new SpotRiskData(JsDate.create(2015, 0, 1, 22, 25, 0), 100, (0.93347 - (0.93347 - 0.93318) * 2 / 3), 1));
        data.add(new SpotRiskData(JsDate.create(2015, 0, 1, 23, 5, 0), -25, 0.93368 - (0.93368 - 0.93354) * 1 / 3, 1));
        data.add(new SpotRiskData(JsDate.create(2015, 0, 2, 1, 3, 0), 40, 0.93426 - (0.93426 - 0.93408) * 3 / 15, 2));
        data.add(new SpotRiskData(JsDate.create(2015, 0, 2, 1, 7, 0), 45, 0.93426 - (0.93426 - 0.93408) * 7 / 15, 2));
        data.add(new SpotRiskData(JsDate.create(2015, 0, 2, 1, 16, 0), 5, 0.93408 - (0.93408 - 0.93422) * 1 / 15, 2));
        data.add(new SpotRiskData(JsDate.create(2015, 0, 2, 1, 31, 0), -9, 0.93422 - (0.93422 - 0.93434) * 1 / 15, 2));
        data.add(new SpotRiskData(JsDate.create(2015, 0, 2, 2, 29, 0), -66, 0.93426 - (0.93426 - 0.93453) * 14 / 15, 2));
        data.add(new SpotRiskData(JsDate.create(2015, 0, 2, 2, 46, 0), -56, 0.93438 - (0.93438 - 93423) * 1 / 15, 2));
        data.add(new SpotRiskData(JsDate.create(2015, 0, 2, 3, 1, 0), 3.33, 0.93423 - (0.93423 - 0.93428) * 1 / 15, 1));
        return data;
    }

    private static class MidMarketData {

        private final JsDate date;
        private final double price;

        @Override
        public String toString() {
            return "Data [date=" + date + ", price=" + price + "]";
        }

        public MidMarketData(JsDate date, double price) {
            this.date = date;
            this.price = price;
        }

        public JsDate getDate() {
            return date;
        }

        public double getPrice() {
            return price;
        }

    }

    private class Margins {

        int left;
        int right;
        int bottom;
        int top;

        public Margins(int left, int right, int bottom, int top) {
            this.left = left;
            this.right = right;
            this.bottom = bottom;
            this.top = top;
        }

    }

    private static class DatumX implements DatumFunction<Double> {
        private final TimeScale scale;

        DatumX(TimeScale scale) {
            this.scale = scale;
        }

        @Override
        public Double apply(final Element context, final Value d, final int index) {
            return scale.apply(d.<MidMarketData>as().getDate()).asDouble();
        }
    }

    private static class DatumY implements DatumFunction<Double> {

        private final LinearScale scale;

        DatumY(LinearScale scale) {
            this.scale = scale;
        }

        @Override
        public Double apply(final Element context, final Value d, final int index) {
            return scale.apply(d.<MidMarketData>as().getPrice()).asDouble();
        }
    }

    private static class SpotRiskData {

        private final JsDate date;
        private final double pnl;
        private final double midMarketPrice;
        private final long clientId;

        private SpotRiskData(JsDate date, double pnl, double midMarketPrice, long clientId) {
            this.date = date;
            this.pnl = pnl;
            this.midMarketPrice = midMarketPrice;
            this.clientId = clientId;
        }

        public JsDate getDate() {
            return date;
        }

        public double getPnl() {
            return pnl;
        }

        public long getClientId() {
            return clientId;
        }

        public double getMidMarketPrice() {
            return midMarketPrice;
        }
    }

}